Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

fix($compile): support merging special attribute names in replace d… #13318

Closed
wants to merge 9 commits into from

Conversation

petebacondarwin
Copy link
Contributor

…irectives

When compiling a replace directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, setAttribute and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as (click) and [value].

This is relevant when using ngForward with Angular Material, since the mgButton
directive uses replace and in the former you often use (click).

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes #13317

@@ -1221,6 +1228,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
};

function setSpecialAttr(element, attrName, value) {
specialAttrHolder.innerHTML = "<span " + attrName + "='" + (value || '').replace(QUOTE_REGEX, '&quot;') + "'>";
var attribute = specialAttrHolder.firstChild.attributes[0].cloneNode();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to MDN, cloneNode() is supposed to be deprecated.

It also says that, since Chrome v45, `Attr` no longer inherits from `Node`, but `cloneNode()` seems to still work on Chrome v46.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, trouble is I can't find any other way of achieving the desired solution. Any other ideas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahah how about this:

specialAttrHolder.innerHTML = "<span " + attrName + "='" + (value || '').replace(QUOTE_REGEX, '&quot;') + "'>"
var span = specialAttrHolder.firstChild;
var attribute = span.attributes[0];
span.removeAttribute(attrName);
element.attributes.setNamedItem(attribute);

This avoids any deprecated API and could possibly be a lot quicker as we are not cloning anything.

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Nov 17, 2015
…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
Closes angular#13318
@timkindberg
Copy link
Contributor

so this performance hit will only happen in certain scenarios? It will not happen for everything using ng-forward special attributes correct?

@@ -989,6 +989,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
$controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) {

var SIMPLE_ATTR_NAME = /^\w/;
var QUOTE_REGEX = /'/g;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks unused...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes. That was from a previous commit in this pr

@petebacondarwin
Copy link
Contributor Author

@jbedard yes. It has no impact for almost all circumstances

@jbedard
Copy link
Collaborator

jbedard commented Nov 17, 2015

That wasn't me asking, but good to know :)

Using innerHTML looks painful but I don't see any other way. Except maybe convincing people to not use replace:true?

@petebacondarwin
Copy link
Contributor Author

Sorry @jbedard - difficult to keep track on a phone.
@timkindberg - yes you are correct. This only occurs if replace: trueis used on a directive AND the user of the directive uses "special" attributes such as (click) or [value]. If ng-forward supported the long form of these attributes, such as bind-value and on-click then the user can always choose to use that form instead on directives that are replace: true and not incur any performance degradation.

I spent ages digging through the DOM API and hacking in different browsers. The final solution we have here is about as fast as I can get and doesn't use any deprecated APIs. But if anyone has time to find a faster solution that would be great.

I'm afraid that we are stuck with replace: true for the time being...

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Nov 17, 2015
…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
Closes angular#13318
@lgalfaso
Copy link
Contributor

@petebacondarwin I wonder if we really have to support this. Would it be possible that if someone needs this, then they have to use the extended notation? My rational is that building html by concatenating strings enlarge the surface for mXSS.

@matsko matsko modified the milestones: 1.5.0-beta.2, 1.5.0-beta.3 Nov 18, 2015
@petebacondarwin
Copy link
Contributor Author

@lgalfaso I see where you are coming from about security.

I think that in this case we are safe though (please do check the accuracy of this):

  • we are creating a single div containing a span, which is never connected to the main document. Any dubious HTML that is injected into this div's innerHTML will never be executable. (Is that correct?)
  • we are using DOM manipulation (span.attributes[0]) to get hold of exactly the attribute object that we created in the innerHTML. Even if additional HTML was injected into the innerHTML it could not bleed through into the destination element.

@lgalfaso
Copy link
Contributor

@petebacondarwin this solution still has issues with attributes with special names (tried this with Chrome) http://plnkr.co/edit/0GjwipSakMskljc7X3Bl?p=preview

Just run it and check the console

@petebacondarwin
Copy link
Contributor Author

@lgalfaso - thanks for spotting that. We have to use the attributes.removeNamedItem(attribute.name) to get it to work properly. Other combinations seem to fail for one reason or another.

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Nov 18, 2015
…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
Closes angular#13318
@petebacondarwin
Copy link
Contributor Author

Added a test and a fix for the problem that @lgalfaso noted

…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
Closes angular#13318
…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
Closes angular#13318
…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
Closes angular#13318
@lgalfaso
Copy link
Contributor

@petebacondarwin can you please remove all the console.log?

@petebacondarwin
Copy link
Contributor Author

The console log are there because there was a failure on Firefox bit I couldn't reproduce it locally. Try pulling the previous commit for the proper solution

@petebacondarwin
Copy link
Contributor Author

Well this is interesting...

Given

<h1 Ω="omega">Hello Plunker!</h1>

the following fails only with JQuery on Firefox

var h1 = $('h1');
expect(h1.attr('Ω')).toEqual('omega');

http://plnkr.co/edit/qrmsTZsW78CYxJSi7Pyv?p=preview

compared to with jqLite:

http://plnkr.co/edit/h9Zfji8HtgcFnGKF49My?p=preview

Is that a bug in JQuery @mzgol?

@mgol
Copy link
Member

mgol commented Nov 18, 2015

Hmm, interesting, native getAttribute works. I'll need to look at it closer. (not today, though)

@mgol
Copy link
Member

mgol commented Nov 18, 2015

@petebacondarwin Seems like a bug, I reported it to the jQuery bug tracker: jquery/jquery#2730

@mgol
Copy link
Member

mgol commented Nov 18, 2015

OK, please read jquery/jquery#2730 (comment). So, jQuery is using String#lowercase on the input which is incorrect according to the spec (citations in the linked comment) but the reason it breaks only in Firefox is that only Firefox is apparently following the spec here...

…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
Closes angular#13318
gkalpak pushed a commit to gkalpak/angular.js that referenced this pull request Nov 23, 2015
…irectives

When compiling a `replace` directive, the compiler merges the attributes from
the replaced element onto the template element.

Unfortunately, `setAttribute` and other related DOM methods do not allow certain
attribute names - in particular Angular 2 style names such as `(click)` and `[value]`.

This is relevant when using ngForward with Angular Material, since the `mgButton`
directive uses `replace` and in the former you often use `(click)`.

This fixes the problem but for those special attributes the speed is considerably
slow.

Closes angular#13317
Closes angular#13318
@petebacondarwin petebacondarwin deleted the issue-13317 branch November 24, 2016 09:06
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

fix(Attribute): allow unconventional attribute names on elements
8 participants